Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 72f623293e09561aa0f7aa69990882b6fb5c9752


Parents : 84e616a
Author : Mark Qvist <mark@unsigned.io>
Date : 2021-07-05T01:15:02+02:00

Initial work on micron markup language. Work on guide section.

Changes
Diff

diff --git a/nomadnet/NomadNetworkApp.py b/nomadnet/NomadNetworkApp.py
index 3204b26..31bf540 100644
--- a/nomadnet/NomadNetworkApp.py
+++ b/nomadnet/NomadNetworkApp.py
@@ -252,6 +252,11 @@ class NomadNetworkApp:
else:
self.config["textui"]["mouse_enabled"] = self.config["textui"].as_bool("mouse_enabled")
+ if not "hide_guide" in self.config["textui"]:
+ self.config["textui"]["hide_guide"] = False
+ else:
+ self.config["textui"]["hide_guide"] = self.config["textui"].as_bool("hide_guide")
+
if not "animation_interval" in self.config["textui"]:
self.config["textui"]["animation_interval"] = 1
else:
@@ -373,6 +378,11 @@ mouse_enabled = True
# alias will be used.
editor = editor
+# If you don't want the Guide section to
+# show up in the menu, you can disable it.
+
+hide_guide = no
+
[node]
enable_node = no

diff --git a/nomadnet/ui/TextUI.py b/nomadnet/ui/TextUI.py
index 63b1689..012f87d 100644
--- a/nomadnet/ui/TextUI.py
+++ b/nomadnet/ui/TextUI.py
@@ -15,31 +15,34 @@ THEME_DARK = 0x01
THEME_LIGHT = 0x02
THEMES = {
- THEME_DARK: [
- # Style name # 16-color style # Monochrome style # 88, 256 and true-color style
- ('heading', 'light gray,underline', 'default', 'underline', 'g93,underline', 'default'),
- ('menubar', 'black', 'light gray', 'standout', '#111', '#bbb'),
- ('shortcutbar', 'black', 'light gray', 'standout', '#111', '#bbb'),
- ('body_text', 'white', 'default', 'default', '#ddd', 'default'),
- ('error_text', 'dark red', 'default', 'default', 'dark red', 'default'),
- ('warning_text', 'yellow', 'default', 'default', '#ba4', 'default'),
- ('inactive_text', 'dark gray', 'default', 'default', 'dark gray', 'default'),
- ('buttons', 'light green,bold', 'default', 'default', '#00a533', 'default'),
- ('msg_editor', 'black', 'light cyan', 'standout', '#111', '#0bb'),
- ("msg_header_ok", 'black', 'light green', 'standout', '#111', '#6b2'),
- ("msg_header_caution", 'black', 'yellow', 'standout', '#111', '#fd3'),
- ("msg_header_sent", 'black', 'light gray', 'standout', '#111', '#ddd'),
- ("msg_header_delivered", 'black', 'light blue', 'standout', '#111', '#28b'),
- ("msg_header_failed", 'black', 'dark gray', 'standout', 'black', 'dark gray'),
- ("msg_warning_untrusted", 'black', 'dark red', 'standout', '#111', 'dark red'),
- ("list_focus", "black", "light gray", "standout", "#111", "#bbb"),
- ("list_off_focus", "black", "dark gray", "standout", "#111", "dark gray"),
- ("list_trusted", "light green", "default", "default", "#6b2", "default"),
- ("list_focus_trusted", "black", "light gray", "standout", "#180", "#bbb"),
- ("list_unknown", "dark gray", "default", "default", "light gray", "default"),
- ("list_untrusted", "dark red", "default", "default", "dark red", "default"),
- ("list_focus_untrusted", "black", "light gray", "standout", "#810", "#bbb"),
- ]
+ THEME_DARK: {
+ "urwid_theme": [
+ # Style name # 16-color style # Monochrome style # 88, 256 and true-color style
+ ('heading', 'light gray,underline', 'default', 'underline', 'g93,underline', 'default'),
+ ('menubar', 'black', 'light gray', 'standout', '#111', '#bbb'),
+ ('shortcutbar', 'black', 'light gray', 'standout', '#111', '#bbb'),
+ ('body_text', 'white', 'default', 'default', '#ddd', 'default'),
+ ('error_text', 'dark red', 'default', 'default', 'dark red', 'default'),
+ ('warning_text', 'yellow', 'default', 'default', '#ba4', 'default'),
+ ('inactive_text', 'dark gray', 'default', 'default', 'dark gray', 'default'),
+ ('buttons', 'light green,bold', 'default', 'default', '#00a533', 'default'),
+ ('msg_editor', 'black', 'light cyan', 'standout', '#111', '#0bb'),
+ ("msg_header_ok", 'black', 'light green', 'standout', '#111', '#6b2'),
+ ("msg_header_caution", 'black', 'yellow', 'standout', '#111', '#fd3'),
+ ("msg_header_sent", 'black', 'light gray', 'standout', '#111', '#ddd'),
+ ("msg_header_delivered", 'black', 'light blue', 'standout', '#111', '#28b'),
+ ("msg_header_failed", 'black', 'dark gray', 'standout', 'black', 'dark gray'),
+ ("msg_warning_untrusted", 'black', 'dark red', 'standout', '#111', 'dark red'),
+ ("list_focus", "black", "light gray", "standout", "#111", "#bbb"),
+ ("list_off_focus", "black", "dark gray", "standout", "#111", "dark gray"),
+ ("list_trusted", "light green", "default", "default", "#6b2", "default"),
+ ("list_focus_trusted", "black", "light gray", "standout", "#180", "#bbb"),
+ ("list_unknown", "dark gray", "default", "default", "light gray", "default"),
+ ("list_normal", "dark gray", "default", "default", "light gray", "default"),
+ ("list_untrusted", "dark red", "default", "default", "dark red", "default"),
+ ("list_focus_untrusted", "black", "light gray", "standout", "#810", "#bbb"),
+ ],
+ }
}
GLYPHSETS = {
@@ -81,12 +84,15 @@ class TextUI:
urwid.set_encoding("UTF-8")
- intro_timeout = self.app.config["textui"]["intro_time"]
- colormode = self.app.config["textui"]["colormode"]
- theme = self.app.config["textui"]["theme"]
- mouse_enabled = self.app.config["textui"]["mouse_enabled"]
+ intro_timeout = self.app.config["textui"]["intro_time"]
+ colormode = self.app.config["textui"]["colormode"]
+ theme = self.app.config["textui"]["theme"]
+ mouse_enabled = self.app.config["textui"]["mouse_enabled"]
- palette = THEMES[theme]
+ self.palette = THEMES[theme]["urwid_theme"]
+
+ for entry in nomadnet.ui.textui.MarkupParser.URWID_THEME:
+ self.palette.append(entry)
if self.app.config["textui"]["glyphs"] == "plain":
glyphset = "plain"
@@ -101,9 +107,8 @@ class TextUI:
for glyph in GLYPHS:
self.glyphs[glyph[0]] = glyph[GLYPHSETS[glyphset]]
-
self.screen = urwid.raw_display.Screen()
- self.screen.register_palette(palette)
+ self.screen.register_palette(self.palette)
self.main_display = Main.MainDisplay(self, self.app)

diff --git a/nomadnet/ui/textui/Config.py b/nomadnet/ui/textui/Config.py
index c2b75cd..6a0ec1f 100644
--- a/nomadnet/ui/textui/Config.py
+++ b/nomadnet/ui/textui/Config.py
@@ -34,7 +34,7 @@ class ConfigDisplay():
self.editor_term.term.change_focus(True)
pile = urwid.Pile([
- urwid.Text(("body_text", "\nTo change the configuration, edit the config file located at:\n\n"+self.app.configpath+"\n\nRestart Nomad Network for chanes to take effect\n"), align="center"),
+ urwid.Text(("body_text", "\nTo change the configuration, edit the config file located at:\n\n"+self.app.configpath+"\n\nRestart Nomad Network for changes to take effect\n"), align="center"),
urwid.Padding(urwid.Button("Open Editor", on_press=open_editor), width=15, align="center"),
])

diff --git a/nomadnet/ui/textui/Guide.py b/nomadnet/ui/textui/Guide.py
new file mode 100644
index 0000000..893a84a
--- /dev/null
+++ b/nomadnet/ui/textui/Guide.py
@@ -0,0 +1,214 @@
+import RNS
+import urwid
+import nomadnet
+from nomadnet.vendor.additional_urwid_widgets import IndicativeListBox, MODIFIER_KEY
+from .MarkupParser import markup_to_attrmaps
+
+class GuideDisplayShortcuts():
+ def __init__(self, app):
+ self.app = app
+ g = app.ui.glyphs
+
+ self.widget = urwid.AttrMap(urwid.Text(""), "shortcutbar")
+
+class ListEntry(urwid.Text):
+ _selectable = True
+
+ signals = ["click"]
+
+ def keypress(self, size, key):
+ """
+ Send 'click' signal on 'activate' command.
+ """
+ if self._command_map[key] != urwid.ACTIVATE:
+ return key
+
+ self._emit('click')
+
+ def mouse_event(self, size, event, button, x, y, focus):
+ """
+ Send 'click' signal on button 1 press.
+ """
+ if button != 1 or not urwid.util.is_mouse_press(event):
+ return False
+
+ self._emit('click')
+ return True
+
+class SelectText(urwid.Text):
+ _selectable = True
+
+ signals = ["click"]
+
+ def keypress(self, size, key):
+ """
+ Send 'click' signal on 'activate' command.
+ """
+ if self._command_map[key] != urwid.ACTIVATE:
+ return key
+
+ self._emit('click')
+
+ def mouse_event(self, size, event, button, x, y, focus):
+ """
+ Send 'click' signal on button 1 press.
+ """
+ if button != 1 or not urwid.util.is_mouse_press(event):
+ return False
+
+ self._emit('click')
+ return True
+
+class GuideEntry(urwid.WidgetWrap):
+ def __init__(self, app, reader, topic_name):
+ self.app = app
+ self.reader = reader
+ g = self.app.ui.glyphs
+
+ widget = ListEntry(topic_name)
+ urwid.connect_signal(widget, "click", self.display_topic, topic_name)
+
+ style = "list_normal"
+ focus_style = "list_focus"
+ self.display_widget = urwid.AttrMap(widget, style, focus_style)
+ urwid.WidgetWrap.__init__(self, self.display_widget)
+
+ def display_topic(self, event, topic):
+ markup = TOPICS[topic]
+ attrmaps = markup_to_attrmaps(markup)
+
+ self.reader.set_content_widgets(attrmaps)
+
+class TopicList(urwid.WidgetWrap):
+ def __init__(self, app, guide_display):
+ self.app = app
+ g = self.app.ui.glyphs
+
+ self.topic_list = [
+ GuideEntry(self.app, guide_display, "Introduction"),
+ GuideEntry(self.app, guide_display, "Conversations"),
+ GuideEntry(self.app, guide_display, "Markup"),
+ GuideEntry(self.app, guide_display, "Licenses & Credits"),
+ ]
+
+ self.ilb = IndicativeListBox(
+ self.topic_list,
+ initialization_is_selection_change=False,
+ )
+
+ urwid.WidgetWrap.__init__(self, urwid.LineBox(self.ilb, title="Topics"))
+
+
+ def keypress(self, size, key):
+ if key == "up" and (self.ilb.first_item_is_selected()):
+ nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.set_focus("header")
+
+ return super(TopicList, self).keypress(size, key)
+
+class GuideDisplay():
+ list_width = 0.33
+
+ def __init__(self, app):
+ self.app = app
+ g = self.app.ui.glyphs
+
+ topic_text = urwid.Text("\nNo topic selected", align="left")
+
+ self.left_area = TopicList(self.app, self)
+ self.right_area = urwid.LineBox(urwid.Filler(topic_text, "top"))
+
+
+ self.columns = urwid.Columns(
+ [
+ ("weight", GuideDisplay.list_width, self.left_area),
+ ("weight", 1-GuideDisplay.list_width, self.right_area)
+ ],
+ dividechars=0, focus_column=0
+ )
+
+ self.shortcuts_display = GuideDisplayShortcuts(self.app)
+ self.widget = self.columns
+
+ def set_content_widgets(self, new_content):
+ options = self.columns.options(width_type="weight", width_amount=1-GuideDisplay.list_width)
+ pile = urwid.Pile(new_content)
+ content = urwid.LineBox(urwid.Filler(pile, "top"))
+
+ self.columns.contents[1] = (content, options)
+
+ def shortcuts(self):
+ return self.shortcuts_display
+
+
+TOPIC_INTRODUCTION = '''>Nomad Network
+
+Communicate Freely.
+
+Nomad Network is built using Reticulum
+-~
+## Notable Features
+ - Encrypted messaging over packet-radio, LoRa, WiFi or anything else [Reticulum](https://github.com/markqvist/Reticulum) supports.
+ - Zero-configuration, minimal-infrastructure mesh communication
+-
+## Current Status
+
+Pre-alpha. At this point Nomad Network is usable as a basic messaging client over Reticulum networks, but only the very core features have been implemented. Development is ongoing and current features being implemented are:
+
+ - Propagated messaging and discussion threads
+ - Connectable nodes that can host pages, files and other resources
+ - Collaborative information sharing and spatial map-style "wikis"
+-
+## Dependencies:
+ - Python 3
+ - RNS
+ - LXMF
+
+```
+
+To use Nomad Network on packet radio or LoRa, you will need to configure your Reticulum installation
+to use any relevant packet radio TNCs or LoRa devices on your system. See the Reticulum documentation
+ for info.
+
+## Caveat Emptor
+Nomad Network is experimental software, and should be considered as such. While it has been built wit
+h cryptography best-practices very foremost in mind, it _has not_ been externally security audited, a
+nd there could very well be privacy-breaking bugs. If you want to help out, or help sponsor an audit,
+ please do get in touch.
+'''
+
+TOPIC_CONVERSATIONS = '''Conversations
+=============
+
+Conversations in Nomad Network
+'''
+
+TOPIC_MARKUP = '''>Markup
+Nomad Network supports a simple and functional markup language called micron. It has a lean markup structure that adds very little overhead, and is still readable as plain text, but offers basic formatting and text structuring, ideal for displaying in a terminal.
+
+Lorem ipsum dolor sit amet.
+
+>>Encoding
+`F222`BdddAll uM source files are encoded as UTF-8, and clients supporting uM display should support UTF-8.
+``
+>>>Sections and `F900Headings`f
+You can define an arbitrary number of sections and sub-sections, each with their own heading
+
+-
+
+Dividers inside section will adhere to section indents
+
+>>>>
+If no heading text is defined, the section will appear as a sub-section without a header.
+
+<-
+Horizontal dividers can be inserted
+
+Text `F2cccan`f be `_underlined`_, `!bold`! or `*italic`*. You `F000`B2cccan`b`f also `_`*`!combine formatting``!
+
+'''
+
+TOPICS = {
+ "Introduction": TOPIC_INTRODUCTION,
+ "Conversations": TOPIC_CONVERSATIONS,
+ "Markup": TOPIC_MARKUP,
+}
\ No newline at end of file

diff --git a/nomadnet/ui/textui/Main.py b/nomadnet/ui/textui/Main.py
index de0fff7..8a6956a 100644
--- a/nomadnet/ui/textui/Main.py
+++ b/nomadnet/ui/textui/Main.py
@@ -7,6 +7,7 @@ from .Directory import *
from .Config import *
from .Map import *
from .Log import *
+from .Guide import *
import urwid
class SubDisplays():
@@ -18,6 +19,7 @@ class SubDisplays():
self.config_display = ConfigDisplay(self.app)
self.map_display = MapDisplay(self.app)
self.log_display = LogDisplay(self.app)
+ self.guide_display = GuideDisplay(self.app)
self.active_display = self.conversations_display
@@ -113,6 +115,10 @@ class MainDisplay():
self.sub_displays.active_display = self.sub_displays.log_display
self.update_active_sub_display()
+ def show_guide(self, user_data):
+ self.sub_displays.active_display = self.sub_displays.guide_display
+ self.update_active_sub_display()
+
def update_active_sub_display(self):
self.frame.contents["body"] = (self.sub_displays.active().widget, None)
self.update_active_shortcuts()
@@ -149,10 +155,15 @@ class MenuDisplay():
button_map = (7, MenuButton("Map", on_press=handler.show_map))
button_log = (7, MenuButton("Log", on_press=handler.show_log))
button_config = (10, MenuButton("Config", on_press=handler.show_config))
+ button_guide = (9, MenuButton("Guide", on_press=handler.show_guide))
button_quit = (8, MenuButton("Quit", on_press=handler.quit))
# buttons = [menu_text, button_conversations, button_node, button_directory, button_map]
- buttons = [menu_text, button_conversations, button_network, button_log, button_config, button_quit]
+ if self.app.config["textui"]["hide_guide"]:
+ buttons = [menu_text, button_conversations, button_network, button_log, button_config, button_quit]
+ else:
+ buttons = [menu_text, button_conversations, button_network, button_log, button_config, button_guide, button_quit]
+
columns = MenuColumns(buttons, dividechars=1)
columns.handler = handler

diff --git a/nomadnet/ui/textui/MarkupParser.py b/nomadnet/ui/textui/MarkupParser.py
new file mode 100644
index 0000000..9aeec63
--- /dev/null
+++ b/nomadnet/ui/textui/MarkupParser.py
@@ -0,0 +1,204 @@
+import nomadnet
+import urwid
+import re
+
+URWID_THEME = [
+ # Style name 16-color style Monochrome style # 88, 256 and true-color style
+ ('plain', 'light gray', 'default', 'default', '#ddd', 'default'),
+ ('heading1', 'black', 'light gray', 'standout', '#222', '#bbb'),
+ ('heading2', 'black', 'light gray', 'standout', '#111', '#999'),
+ ('heading3', 'black', 'light gray', 'standout', '#000', '#777'),
+ ('f_underline', 'default,underline', 'default', 'default,underline', 'default,underline', 'default'),
+ ('f_bold', 'default,bold', 'default', 'default,bold', 'default,bold', 'default'),
+ ('f_italic', 'default,italics', 'default', 'default,italics', 'default,italics', 'default'),
+]
+
+SYNTH_STYLES = []
+
+SECTION_INDENT = 2
+INDENT_RIGHT = 1
+
+def markup_to_attrmaps(markup):
+ attrmaps = []
+ global_style = ""
+
+ state = {
+ "depth": 0,
+ "fg_color": "default",
+ "bg_color": "default",
+ "formatting": {
+ "bold": False,
+ "underline": False,
+ "italic": False,
+ "strikethrough": False,
+ "blink": False,
+ }
+ }
+
+ # Split entire document into lines for
+ # processing.
+ lines = markup.split("\n");
+
+ for line in lines:
+ if len(line) > 0:
+ display_widget = parse_line(line, state)
+ else:
+ display_widget = urwid.Text("")
+
+ if global_style == "":
+ global_style = "plain"
+
+ if display_widget != None:
+ attrmap = urwid.AttrMap(display_widget, global_style)
+ attrmaps.append(attrmap)
+
+ return attrmaps
+
+
+def parse_line(line, state):
+ first_char = line[0]
+
+ # Check if the command is an escape
+ if first_char == "\\":
+ line = line[1:]
+
+ # Check for section heading reset
+ elif first_char == "<":
+ state["depth"] = 0
+ return parse_line(line[1:], state)
+
+ # Check for section headings
+ elif first_char == ">":
+ i = 0
+ while i < len(line) and line[i] == ">":
+ i += 1
+ state["depth"] = i
+
+ for j in range(1, i+1):
+ wanted_style = "heading"+str(i)
+ if any(s[0]==wanted_style for s in URWID_THEME):
+ style = wanted_style
+
+ line = line[state["depth"]:]
+ if len(line) > 0:
+ line = " "*left_indent(state)+line
+ return urwid.AttrMap(urwid.Text(line), style)
+ else:
+ return None
+
+ # Check for horizontal dividers
+ elif first_char == "-":
+ if len(line) == 2:
+ divider_char = line[1]
+ else:
+ divider_char = "\u2500"
+ if state["depth"] == 0:
+ return urwid.Divider(divider_char)
+ else:
+ return urwid.Padding(urwid.Divider(divider_char), left=left_indent(state), right=right_indent(state))
+
+ output = make_output(state, line)
+
+ if state["depth"] == 0:
+ return urwid.Text(output)
+ else:
+ return urwid.Padding(urwid.Text(output), left=left_indent(state), right=right_indent(state))
+
+def left_indent(state):
+ return (state["depth"]-1)*SECTION_INDENT
+
+def right_indent(state):
+ return (state["depth"]-1)*SECTION_INDENT
+
+def make_part(state, part):
+ return (make_style(state), part)
+
+def make_style(state):
+ def mono_color(fg, bg):
+ return "default"
+ def low_color(color):
+ # TODO: Implement
+ return "default"
+ def high_color(color):
+ if color == "default":
+ return color
+ else:
+ return "#"+color
+
+ bold = state["formatting"]["bold"]
+ underline = state["formatting"]["underline"]
+ italic = state["formatting"]["italic"]
+ fg = state["fg_color"]
+ bg = state["bg_color"]
+
+ format_string = ""
+ if bold:
+ format_string += ",bold"
+ if underline:
+ format_string += ",underline"
+ if italic:
+ format_string += ",italics"
+
+ name = ""+fg+","+bg+","+format_string
+ if not name in SYNTH_STYLES:
+ screen = nomadnet.NomadNetworkApp.get_shared_instance().ui.screen
+ screen.register_palette_entry(name, low_color(fg)+format_string,low_color(bg),mono_color(fg, bg)+format_string,high_color(fg)+format_string,high_color(bg))
+ SYNTH_STYLES.append(name)
+
+ return name
+
+def make_output(state, line):
+ output = []
+ part = ""
+ mode = "text"
+ skip = 0
+ for i in range(0, len(line)):
+ c = line[i]
+ if skip > 0:
+ skip -= 1
+ else:
+ if mode == "formatting":
+ if c == "_":
+ state["formatting"]["underline"] ^= True
+ elif c == "!":
+ state["formatting"]["bold"] ^= True
+ elif c == "*":
+ state["formatting"]["italic"] ^= True
+ elif c == "F":
+ if len(line) > i+4:
+ color = line[i+1:i+4]
+ state["fg_color"] = color
+ skip = 3
+ elif c == "f":
+ state["fg_color"] = "default"
+ elif c == "B":
+ if len(line) > i+4:
+ color = line[i+1:i+4]
+ state["bg_color"] = color
+ skip = 3
+ elif c == "b":
+ state["bg_color"] = "default"
+ elif c == "`":
+ state["formatting"]["bold"] = False
+ state["formatting"]["underline"] = False
+ state["formatting"]["italic"] = False
+ state["fg_color"] = "default"
+ state["bg_color"] = "default"
+
+ mode = "text"
+ if len(part) > 0:
+ output.append(make_part(state, part))
+
+ elif mode == "text":
+ if c == "`":
+ mode = "formatting"
+ if len(part) > 0:
+ output.append(make_part(state, part))
+ part = ""
+ else:
+ part += c
+
+ if i == len(line)-1:
+ output.append(make_part(state, part))
+
+ return output
\ No newline at end of file


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────